package cpaparser;

import java.util.Vector;
import java.util.HashMap;

import javax.xml.parsers.*;

import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;

import cpagraph.*;

import java.io.*;

import javax.microedition.rms.*;

/**
 * This class implements the HandlerBase helper class, which means
 * that it defines all the "callback" methods that the SAX parser will
 * invoke to notify the application.  In this example we override the 
 * methods that we require.
 **/
public class XmlParser extends DefaultHandler
{
	private String lastType = "";
	private OSMNode lastNode = null;
	private Way lastWay = null;
	private Relation lastRel = null;
	private Vector<OSMNode> nodes = new Vector<OSMNode>();
	private Vector<Way> ways = new Vector<Way>();
	private Vector<Relation> relations = new Vector<Relation>();
	private HashMap<String, OSMNode> hashNodes = new HashMap<String, OSMNode>();
	private HashMap<String, Way> hashWays = new HashMap<String, Way>();
	private HashMap<String, Node> hashGraphNodes = new HashMap<String, Node>();
	private Graph graph;
	private HashMap<String, Bus> busses;
	private String fileGraphDB = "graphdb";
	
	public XmlParser()
	{
		super();
	}
	
	public static void main(String [] args)
	{
		if (args.length < 1)
		{
			System.err.println("Usage: XmlParser <filename>");
			System.exit(1);
		}
		String filename = args[0];		
		SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
		try
		{
	    	SAXParser saxParser = saxParserFactory.newSAXParser();
	    	XMLReader parser = saxParser.getXMLReader();
	    	parser.setContentHandler(new XmlParser()); 
	    	parser.parse(new InputSource(new FileInputStream(filename)));
		}
		catch (SAXException se)
		{
			se.printStackTrace();
		}
		catch (FileNotFoundException fe)
		{
			System.err.println("File could not be opened.");
			fe.printStackTrace();
		}
		catch (IOException ie)
		{
			ie.printStackTrace();
		}
		catch (ParserConfigurationException pe)
		{
			pe.printStackTrace();
		}
	}
	
	// When the parser encounters plain text (not XML elements), it calls
	// this method, which accumulates them in a string buffer
	final public void characters (final char [] ch, final int start, final int len)
	{
		//System.out.println(new String(ch, start, len).trim());
	}

	// Every time the parser encounters the beginning of a new element, it
	// calls this method, which resets the string buffer
	public void startElement (final String namespace, final String localname, final String type, final Attributes attributes)
			throws SAXException 
	{
		//System.out.println("reading element " + type);
		if (type.equals("node"))
		{
			OSMNode n = new OSMNode(attributes.getValue("id"), Double.parseDouble(attributes.getValue("lat")),
					Double.parseDouble(attributes.getValue("lon")));
			nodes.add(n);
			hashNodes.put(n.id, n);
			lastNode = n;
			lastType = "n";
		}
		else if (type.equals("way"))
		{
			Way w = new Way(attributes.getValue("id"));
			ways.add(w);
			hashWays.put(w.id, w);
			lastWay = w;
			lastType = "w";
		}
		else if (type.equals("relation"))
		{
			Relation r = new Relation(attributes.getValue("id"));
			relations.add(r);
			lastRel = r;
			lastType = "r";
		}
		else if (type.equals("tag"))
		{
			if (lastType.compareToIgnoreCase("n") == 0)
				lastNode.addTag(attributes.getValue("k"), attributes.getValue("v"));
			else if (lastType.compareToIgnoreCase("w") == 0)
				lastWay.addTag(attributes.getValue("k"), attributes.getValue("v"));
			else if (lastType.compareToIgnoreCase("r") == 0)
				lastRel.addTag(attributes.getValue("k"), attributes.getValue("v"));
			else
				System.err.println("Unexpected tag.");
			//System.out.print(attributes.getValue("k"));
			//System.out.println(attributes.getValue("v"));
		}
		else if (type.equals("nd"))
		{
			lastWay.addNode(attributes.getValue("ref"));
		}
		else if (type.equals("member"))
		{
			lastRel.addMember(attributes.getValue("type"), attributes.getValue("ref"),
					attributes.getValue("role"));
		}
	}

	// When the parser encounters the end of an element, it calls this method
	final public void endElement (final String namespace, final String localname, final String type)
		throws SAXException 
	{
		if (type.equals("osm"))
		{
			System.out.println("finished processing document");
			processOutput();
		}
		else if (type.equals("node"))
		{
			lastType = "";
			lastNode = null;
		}
		else if (type.equals("way"))
		{
			lastType = "";
			lastWay = null;
		}
		else if (type.equals("relation"))
		{
			lastType = "";
			lastRel = null;
		}
	}
	
	/**
	 * Find Node object from graph by given id.
	 */
	public Node findNode(String id)
	{
		return hashGraphNodes.get(id);
	}
	
	/**
	 * Find Way object by given id.
	 */
	public Way findWay(String id)
	{
		return hashWays.get(id);
	}
	
	/**
	 * Maps a given ID-String to an OSMNode object.
	 * 
	 * @param id ID of the node we are searching for
	 * @return OSMNode object representing the parsed node from xml file
	 */
	public OSMNode findOSMNode(String id)
	{
		return hashNodes.get(id);
	}
	
	/**
	 * Does everything after xml file being parsed
	 */
	public void processOutput()
	{
		System.out.println("Node Count: " + nodes.size());
		System.out.println("Ways Count: " + ways.size());
		System.out.println("Relations Count: " + relations.size());
		
		makeGraph();
		assignBusLines();
		reduceGraph();
		
		/*
		 * finally strip all disconnected nodes,
		 * make it a connected graph.
		 */
		for (int i = graph.nodes.size() - 1; i >= 0; i--)
		{
			if (!graph.nodes.elementAt(i).isConnected())
				graph.nodes.remove(i);
		}
		
		/*
		 * NOTE
		 * 
		 * produces graph data.
		 * 
		 * Data returned by graph.toRecords()
		 * should be stored into a file using RMS.
		 * 
		 * It is printed to stdout just for testing/debugging
		 * can be removed, when data is successfully written elsewhere.
		 */
		Vector<String> data = graph.toRecords();
		for (int i = 0; i < data.size(); i++)
		{
			System.out.println(data.elementAt(i));
		}
		
		/*
		 * NOTE
		 * 
		 * produces busstops, incudes name of each stop and ref.
		 * 
		 * stops returned by graph.busStopsToRecords()
		 * should be stored into a file using RMS.
		 * 
		 * It is printed to stdout just for testing/debugging
		 * can be removed, when data is successfully written elsewhere.
		 */
		Vector<String> stops = graph.busStopsToRecords();
		for (int i = 0; i < stops.size(); i++)
		{
			System.out.println(stops.elementAt(i));
		}
	}
	
	/**
	 * Creates new Graph and inserts first all OSMNodes parsed by the saxparser.
	 * Then adds edges according to Ways, so a path in the graph denotes a part of one or more Ways. 
	 */
	public void makeGraph()
	{
		graph = new Graph();
		
		/*
		 * adding all nodes
		 */
		for (int i = 0; i < nodes.size(); i++)
		{
			Node newNode = new Node(nodes.elementAt(i));
			
			graph.addNode(newNode);
			hashGraphNodes.put(nodes.elementAt(i).id, newNode);
		}
		
		System.out.println("Nodes in graph: " + Integer.toString(graph.nodes.size()));
		
		/*
		 * chain all the Nodes included in a Way.
		 */
		int countEdges = 0;
		for (int i = 0; i < ways.size(); i++)
		{
			Way w = ways.elementAt(i);
			
			for (int k = 1; k < w.nodes.size(); k++)
			{
				Node n1 = hashGraphNodes.get(w.nodes.elementAt(k-1));
				Node n2 = hashGraphNodes.get(w.nodes.elementAt(k));
				
				graph.addEdge(n1, n2,
						computeDistance(n1.osmNode.lat, n1.osmNode.lon, n2.osmNode.lat, n2.osmNode.lon));
				countEdges++;
			}
		}
		
		System.out.println("Edges in graph: " + Integer.toString(countEdges));
	}
	
	/**
	 * This method creates a busline from ref tag, this bus is then assigned to all nodes used by this relation.
	 * Therefore all ways referenced by the relation must be found.
	 * Secondly all node-members of the relation are connected to the graph in that way,
	 * that the closest node from the relation is searched and connected to it. This ensures both,
	 * finally a connected graph and those bus stops are adjacent only to nodes from the same busline. 
	 */
	public void assignBusLines()
	{
		busses = new HashMap<String, Bus>();
		
		for (int i = 0; i < relations.size(); i++)
		{
			Relation r = relations.elementAt(i);
			String busline = r.getValue("ref");
			
			Bus b;
			if (busses.containsKey(busline))
			{
				b = busses.get(busline);
			}
			else
			{
				b = new Bus(busline);
				busses.put(busline, b);
			}
			
			Vector<Node> nodesInRelation = new Vector<Node>();
			
			for (int j = 0; j < r.members.size(); j++)
			{
				Member m = r.members.elementAt(j);
				
				if (m.type.equals("way"))
				{
					Way w = findWay(m.ref);
					if (w == null)
					{
						System.out.println("Could not find Way " + m.ref + ", occurs in relation " + r.id);
						continue;
					}
					
					for (int k = 0; k < w.nodes.size(); k++)
					{
						Node n = findNode(w.nodes.elementAt(k));
						
						n.assignBus(b);
						nodesInRelation.add(n);
					}
				}
			}
			
			for (int j = 0; j < r.members.size(); j++)
			{
				Member m = r.members.elementAt(j);
			
				if (m.type.equals("node"))
				{
					Node n = this.findNode(m.ref);
					
					if (n == null)
					{
						System.out.println("Could not find Node " + m.ref + ", occurs in relation " + r.id);
						continue;
					}
					
					n.assignBus(b);
					
					Node closestNode = null;
					double minDistance = 9999.99;
					
					for (int k = 0; k < nodesInRelation.size(); k++)
					{
						Node node = nodesInRelation.elementAt(k);
						double distance = computeDistance(node.osmNode.lat, node.osmNode.lon, n.osmNode.lat, n.osmNode.lon);
						
						if (minDistance > distance)
						{
							minDistance = distance;
							closestNode = node;
						}
					}
					
					closestNode.mergeNode(n);
				}
			}
		}
		
		System.out.print("Number of buslines: ");
		System.out.println(busses.size());
	}
	
	/**
	 * Simply deletes all non-busstop Nodes from the graph.
	 * This works, because graph.deleteNode() repairs the graph structure,
	 * and sums weights of merged edges.
	 */
	public void reduceGraph()
	{
		for (int i = graph.nodes.size() - 1; i >= 0; i--)
		{
			Node n = graph.nodes.elementAt(i);
			
			if (!n.isBusStation)
			{
				graph.deleteNode2(n);
				//break;
			}
		}
		
		System.out.println("Nodes in reduced graph: " + Integer.toString(graph.nodes.size()));
	}
	
	public void writeDB()
	{
		RecordStore graphDB = null;
		
		try {
			graphDB = RecordStore.openRecordStore(fileGraphDB, true);
		} catch (UnsatisfiedLinkError e) {
			System.out.println(e);
			System.out.println("strange error...");
			e.printStackTrace();
		} catch (Exception e) {
			System.out.println(e);
			e.printStackTrace();
		}
		
		try {
			graphDB.closeRecordStore();
		} catch (Exception e) {
			System.out.println(e);
			e.printStackTrace();
		}
	}
	
	public static double computeDistance(double lat1, double lon1, double lat2, double lon2)
	{
		double R = 6371; // earth's radius in km
		double dLat = Math.toRadians(lat2-lat1);
		double dLon = Math.toRadians(lon2-lon1); 
		double a = Math.sin(dLat/2) * Math.sin(dLat/2) +
		        Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) * 
		        Math.sin(dLon/2) * Math.sin(dLon/2); 
		double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); 
		return R * c;
	}
	
	/** This method is called when warnings occur */
	public void warning(SAXParseException exception)
	{
		System.err.println("WARNING: line " + exception.getLineNumber() + ": " +
				exception.getMessage());
		//KodalfaSilverGPRS.logStackTrace(exception);
	}

	/** This method is called when errors occur */
	public void error(SAXParseException exception)
	{
		System.err.println("ERROR: line " + exception.getLineNumber() + ": " +
				exception.getMessage());
		//KodalfaSilverGPRS.logStackTrace(exception);
	}

	/** This method is called when non-recoverable errors occur. */
	public void fatalError(SAXParseException exception) throws SAXException
	{
		System.err.println("FATAL: line " + exception.getLineNumber() + ": " +
				exception.getMessage());
		//KodalfaSilverGPRS.logStackTrace(exception);
		throw(exception);
	}
}
